package com.huawei.boss.scripts.jsr223;

import java.io.IOException;

import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import org.apache.log4j.Logger;
import org.springframework.scripting.ScriptCompilationException;
import org.springframework.scripting.ScriptFactory;
import org.springframework.scripting.ScriptSource;
import org.springframework.util.Assert;

import com.huawei.boss.lang.Strings;

public class AbstractJSR223ScriptFactory implements ScriptFactory {
    protected final Logger LOGGER = Logger.getLogger(this.getClass());
    protected final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
    private final Object scriptClassMonitor = new Object();
    private String language;
    private String scriptSourceLocator;
    private Class[] scriptInterfaces;
    private CachedResultHolder cachedResult;

    public void setLanguage(String language) {
        this.language = language;
    }

    public AbstractJSR223ScriptFactory(String scriptSourceLocator, Class[] scriptInterfaces) {
        Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty");
        Assert.notEmpty(scriptInterfaces, "'scriptInterfaces' must not be empty");
        this.scriptSourceLocator = scriptSourceLocator;
        this.scriptInterfaces = scriptInterfaces;
    }

    @Override
    public String getScriptSourceLocator() {
        return this.scriptSourceLocator;
    }

    @Override
    public Class[] getScriptInterfaces() {
        return this.scriptInterfaces;
    }

    @Override
    public boolean requiresConfigInterface() {
        return false;
    }

    @Override
    public Object getScriptedObject(ScriptSource scriptSource, Class[] actualInterfaces) throws IOException,
            ScriptCompilationException {
        synchronized (scriptClassMonitor) {
            if (this.cachedResult == null || scriptSource.isModified()) {
                Assert.hasText(language, "language must not be empty");
                ScriptEngine scriptEngine = scriptEngineManager.getEngineByName(this.language);
                if (scriptEngine == null) {
                    String errorMsg = invlalidLanguageMessage(this.language);
                    LOGGER.error(errorMsg);
                    throw new ScriptCompilationException(errorMsg);
                }

                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("using script engine : " + scriptEngine.getFactory().getEngineName());

                    ScriptEngineFactory factory = scriptEngine.getFactory();
                    LOGGER.info("Loading Script Engine...");
                    LOGGER.info("Engine name: " + factory.getEngineName());
                    LOGGER.info("Engine names: " + Strings.join(factory.getNames(), ","));
                    LOGGER.info("Engine version: " + factory.getEngineVersion());
                    LOGGER.info("Language name: " + factory.getLanguageName());
                    LOGGER.info("Language version: " + factory.getLanguageVersion());
                    LOGGER.info("Language extensions: " + Strings.join(factory.getExtensions(), ","));
                    LOGGER.info("Language mimetypes: " + Strings.join(factory.getMimeTypes(), ","));
                    LOGGER.info("Language thread feautre: " + factory.getParameter("THREADING"));
                    LOGGER.info("Language invocable feautre: " + (scriptEngine instanceof Invocable));
                    LOGGER.info("Language compilable feautre: " + (scriptEngine instanceof Compilable));
                }

                String script = scriptSource.getScriptAsString();
                try {
                    if (scriptEngine instanceof Compilable) {
                        CompiledScript cs = ((Compilable) scriptEngine).compile(script);
                        cs.eval();//编译脚本
                    } else {
                        scriptEngine.eval(script);//解释脚本
                    }
                } catch (ScriptException e) {
                    return new ScriptCompilationException(scriptSource, e);
                }

                for (Class actualInterface : actualInterfaces) {
                    Object obj = ((Invocable) scriptEngine).getInterface(actualInterface);
                    if (obj != null) {
                        this.cachedResult = new CachedResultHolder(obj);
                        return obj;
                    }
                }
            }

            if (this.cachedResult != null) {
                return this.cachedResult.object;
            }
        }
        return null;
    }

    @Override
    public Class getScriptedObjectType(ScriptSource scriptSource) throws IOException, ScriptCompilationException {
        return null;
    }

    @Override
    public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) {
        synchronized (this.scriptClassMonitor) {
            return scriptSource.isModified();
        }
    }

    @Override
    public String toString() {
        return "AbstractScriptFactory: script source locator [" + this.scriptSourceLocator + "]";
    }

    private static String invlalidLanguageMessage(String language) {
        return new StringBuilder().append(ScriptEngineManager.class.getName())
                .append(" is unable to create a script engine for language '").append(language).append("'.\n")
                .append("This may be due to a missing language implementation or an invalid language name.").toString();
    }

    /**
     * Wrapper that holds a temporarily cached result object.
     */
    private static class CachedResultHolder {
        public final Object object;

        public CachedResultHolder(Object object) {
            this.object = object;
        }
    }
}
